#[macro_use]
extern crate bitflags;
use libssh_sys::*;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
use std::sync::{Arc, Mutex, MutexGuard};
use thiserror::Error;

pub const SSH_REQUEST_AUTH: i32 = libssh::ssh_requests_e_SSH_REQUEST_AUTH as i32;
pub const SSH_REQUEST_CHANNEL_OPEN: i32 = libssh::ssh_requests_e_SSH_REQUEST_CHANNEL_OPEN as i32;
pub const SSH_REQUEST_CHANNEL: i32 = libssh::ssh_requests_e_SSH_REQUEST_CHANNEL as i32;
pub const SSH_REQUEST_SERVICE: i32 = libssh::ssh_requests_e_SSH_REQUEST_SERVICE as i32;
pub const SSH_REQUEST_GLOBAL: i32 = libssh::ssh_requests_e_SSH_REQUEST_GLOBAL as i32;

pub const SSH_CHANNEL_UNKNOW: i32 = libssh::ssh_channel_type_e_SSH_CHANNEL_UNKNOWN as i32;
pub const SSH_CHANNEL_SESSION: i32 = libssh::ssh_channel_type_e_SSH_CHANNEL_SESSION as i32;
pub const SSH_CHANNEL_DIRECT_TCPIP: i32 =
    libssh::ssh_channel_type_e_SSH_CHANNEL_DIRECT_TCPIP as i32;
pub const SSH_CHANNEL_FORWARDED_TCPIP: i32 =
    libssh::ssh_channel_type_e_SSH_CHANNEL_FORWARDED_TCPIP as i32;
pub const SSH_CHANNEL_X11: i32 = libssh::ssh_channel_type_e_SSH_CHANNEL_X11 as i32;
pub const SSH_CHANNEL_AUTH_AGENT: i32 = libssh::ssh_channel_type_e_SSH_CHANNEL_AUTH_AGENT as i32;

pub const SSH_CHANNEL_REQUEST_UNKNOWN: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_UNKNOWN as i32;
pub const SSH_CHANNEL_REQUEST_PTY: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_PTY as i32;
pub const SSH_CHANNEL_REQUEST_EXEC: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_EXEC as i32;
pub const SSH_CHANNEL_REQUEST_SHELL: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_SHELL as i32;
pub const SSH_CHANNEL_REQUEST_ENV: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_ENV as i32;
pub const SSH_CHANNEL_REQUEST_SUBSYSTEM: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_SUBSYSTEM as i32;
pub const SSH_CHANNEL_REQUEST_WINDOW_CHANGE: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_WINDOW_CHANGE as i32;
pub const SSH_CHANNEL_REQUEST_X11: i32 =
    libssh::ssh_channel_requests_e_SSH_CHANNEL_REQUEST_X11 as i32;

pub use libssh::SSH_AUTH_METHOD_GSSAPI_MIC;
pub use libssh::SSH_AUTH_METHOD_HOSTBASED;
pub use libssh::SSH_AUTH_METHOD_INTERACTIVE;
pub use libssh::SSH_AUTH_METHOD_NONE;
pub use libssh::SSH_AUTH_METHOD_PASSWORD;
pub use libssh::SSH_AUTH_METHOD_PUBLICKEY;
pub use libssh::SSH_AUTH_METHOD_UNKNOWN;

bitflags! {
    pub struct AuthMethod: u32 {
        const NONE = 1;
        const PASSWORD = 2;
        const PUBLICKEY = 4;
        const HOSTBASED = 8;
        const INTERACTIVE = 16;
        const GSSAPI_MIC = 32;
    }
}

#[repr(C)]
pub struct ChannelPtyCallbacks {
    channel_data_function: extern "C" fn(
        libssh::ssh_session,
        libssh::ssh_channel,
        *mut c_void,
        u32,
        i32,
        *mut c_void,
    ) -> i32,
    channel_eof_function:
        extern "C" fn(callbacks::ssh_session, callbacks::ssh_channel, *mut c_void),
    channel_close_function:
        extern "C" fn(callbacks::ssh_session, callbacks::ssh_channel, *mut c_void),
    userdata: *mut c_void,
}

pub struct ChannnelCallbacks {
    raw: callbacks::ssh_channel_callbacks_struct,
}

impl ChannnelCallbacks {
    pub fn new() -> Self {
        let cb = callbacks::ssh_channel_callbacks_struct {
            size: 0,
            userdata: std::ptr::null_mut(),
            channel_data_function: None,
            channel_eof_function: None,
            channel_close_function: None,
            channel_signal_function: None,
            channel_exit_status_function: None,
            channel_exit_signal_function: None,
            channel_pty_request_function: None,
            channel_shell_request_function: None,
            channel_auth_agent_req_function: None,
            channel_x11_req_function: None,
            channel_pty_window_change_function: None,
            channel_exec_request_function: None,
            channel_env_request_function: None,
            channel_subsystem_request_function: None,
            channel_write_wontblock_function: None,
        };
        ChannnelCallbacks { raw: cb }
    }
    pub fn set_channel_data_function(
        &mut self,
        f: extern "C" fn(
            callbacks::ssh_session,
            callbacks::ssh_channel,
            *mut c_void,
            u32,
            i32,
            *mut c_void,
        ) -> i32,
    ) {
        self.raw.channel_data_function = Some(f);
    }
    pub fn set_channel_eof_function(
        &mut self,
        f: extern "C" fn(callbacks::ssh_session, callbacks::ssh_channel, *mut c_void),
    ) {
        self.raw.channel_eof_function = Some(f);
    }

    pub fn set_channel_close_function(
        &mut self,
        f: extern "C" fn(callbacks::ssh_session, callbacks::ssh_channel, *mut c_void),
    ) {
        self.raw.channel_close_function = Some(f);
    }
}

#[derive(Debug)]
pub struct Key {
    raw: libssh::ssh_key,
}

impl Key {
    pub fn generate_rsa(len: i32) -> Key {
        unsafe {
            let mut key = std::ptr::null_mut();
            libssh::ssh_pki_generate(libssh::ssh_keytypes_e_SSH_KEYTYPE_RSA, len, &mut key);
            Key { raw: key }
        }
    }
    pub fn export_to_file(&mut self, file: &str) -> Result<(), std::io::Error> {
        let file = CString::new(file)?;
        let rc = unsafe {
            libssh::ssh_pki_export_privkey_file(
                self.raw,
                std::ptr::null_mut(),
                None,
                std::ptr::null_mut(),
                file.as_ptr(),
            )
        };
        if rc == libssh::SSH_ERROR {
            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("Error saving key: {}", rc),
            ))
        } else {
            Ok(())
        }
    }
}

#[derive(Clone)]
pub struct Session {
    raw: Arc<Mutex<libssh::ssh_session>>,
}

impl Session {
    pub fn new() -> Self {
        let raw = unsafe { libssh::ssh_new() };
        Session {
            raw: Arc::new(Mutex::new(raw)),
        }
    }
    pub fn raw(&self) -> MutexGuard<libssh::ssh_session> {
        self.raw.lock().expect("aquire raw session lock")
    }

    pub fn set_host(&self, host: &str) {
        let host = CString::new(host).unwrap();
        self.set_ssh_option(libssh::ssh_options_e_SSH_OPTIONS_HOST, host.as_ptr());
    }

    pub fn set_port(&self, port: &usize) {
        self.set_ssh_option(
            libssh::ssh_options_e_SSH_OPTIONS_PORT,
            port as *const usize as *const c_void,
        );
    }
    pub fn set_verbosity(&self, level: &str) {
        let level = CString::new(level).unwrap();
        self.set_ssh_option(
            libssh::ssh_options_e_SSH_OPTIONS_LOG_VERBOSITY,
            level.as_ptr(),
        )
    }

    pub fn set_user(&self, user: &str) {
        let user = CString::new(user).unwrap();
        self.set_ssh_option(libssh::ssh_options_e_SSH_OPTIONS_USER, user.as_ptr())
    }

    pub fn set_ssh_option<T>(&self, option: libssh::ssh_options_e, value: *const T) {
        let session = self.raw();
        let rc = unsafe { libssh::ssh_options_set(*session, option, value as *const c_void) };
        if rc != 0 {
            panic!("Error {} setting ssh session option", rc);
        }
    }

    pub fn connect(&self) -> Result<(), Error> {
        let raw = self.raw();
        let rc = unsafe { libssh::ssh_connect(*raw) };
        self.eval_result(rc)
    }

    pub fn userauth_password(&self, password: &str) -> Result<(), Error> {
        let password = CString::new(password).unwrap();
        let raw = self.raw();
        let rc =
            unsafe { libssh::ssh_userauth_password(*raw, std::ptr::null(), password.as_ptr()) };
        self.eval_result(rc)
    }

    pub fn handle_key_exchange(&self) -> Result<(), Error> {
        let raw = self.raw();
        let rc = unsafe { server::ssh_handle_key_exchange(*raw as server::ssh_session) };
        self.eval_result(rc)
    }

    pub fn disconnect(&self) {
        let raw = self.raw();
        unsafe { libssh::ssh_disconnect(*raw) }
    }

    pub fn is_connected(&self) -> bool {
        let raw = self.raw();
        let rc = unsafe { libssh::ssh_is_connected(*raw) };
        rc == 1
    }

    pub fn get_message(&self) -> Message {
        let raw = self.raw();
        let raw_msg = unsafe { libssh::ssh_message_get(*raw) };
        Message {
            raw: raw_msg,
            session: &self,
        }
    }

    pub fn channel_listen_forward(&self, port: i32) -> Result<(), Error> {
        let raw = self.raw();
        let rc = unsafe {
            libssh::ssh_channel_listen_forward(*raw, std::ptr::null(), port, std::ptr::null_mut())
        };
        self.eval_result(rc)
    }

    fn eval_result(&self, rc: i32) -> Result<(), Error> {
        if rc == libssh::SSH_OK as i32 {
            Ok(())
        } else {
            Err(Error::from_session(&self))
        }
    }
}

impl Drop for Session {
    fn drop(&mut self) {
        if !self.is_connected() {
            self.disconnect();
        }
        let raw = self.raw();
        unsafe {
            libssh::ssh_free(*raw);
        }
    }
}

pub struct Message<'a> {
    raw: libssh::ssh_message,
    session: &'a Session,
}

impl<'a> Message<'a> {
    pub fn subtype(&self) -> i32 {
        unsafe { libssh::ssh_message_subtype(self.raw) }
    }
    pub fn main_type(&self) -> i32 {
        unsafe { libssh::ssh_message_type(self.raw) }
    }
    pub fn auth_reply_success(&self, code: i32) {
        unsafe {
            server::ssh_message_auth_reply_success(self.raw as server::ssh_message, code);
        }
    }
    pub fn auth_reply_default(&self) {
        unsafe {
            server::ssh_message_reply_default(self.raw as server::ssh_message);
        }
    }
    pub fn auth_user(&self) -> String {
        let ptr = unsafe {
            std::ffi::CStr::from_ptr(server::ssh_message_auth_user(
                self.raw as server::ssh_message,
            ))
        };
        ptr.to_str()
            .expect("could not extract user name")
            .to_owned()
    }
    pub fn auth_set_methods(&self, auth_methods: AuthMethod) {
        unsafe {
            server::ssh_message_auth_set_methods(
                self.raw as server::ssh_message,
                auth_methods.bits() as i32,
            );
        }
    }
    pub fn auth_password(&self) -> String {
        let ptr = unsafe {
            std::ffi::CStr::from_ptr(server::ssh_message_auth_password(
                self.raw as server::ssh_message,
            ))
        };
        ptr.to_str().expect("could not extract password").to_owned()
    }
    pub fn channel_request_open_reply_accept(&self) -> Channel {
        let raw_chan = unsafe { libssh::ssh_message_channel_request_open_reply_accept(self.raw) };
        Channel {
            raw: raw_chan,
            session: self.session.clone(),
        }
    }
    pub fn channel_request_reply_success(&self) {
        unsafe {
            libssh::ssh_message_channel_request_reply_success(self.raw);
        }
    }

    pub fn reply_default(&self) {
        unsafe {
            server::ssh_message_reply_default(self.raw as server::ssh_message);
        }
    }
}

impl<'a> Drop for Message<'a> {
    fn drop(&mut self) {
        unsafe { libssh::ssh_message_free(self.raw) }
    }
}

pub struct Channel {
    raw: libssh::ssh_channel,
    session: Session,
}

impl Channel {
    pub fn new(session: &Session) -> Self {
        let raw_sess = session.raw();
        let raw = unsafe { libssh::ssh_channel_new(*raw_sess) };
        Channel {
            raw,
            session: session.clone(),
        }
    }

    pub fn accept_forward(session: &Session, timeout_ms: i32, port: &i32) -> Self {
        let raw = session.raw();
        let channel = unsafe {
            libssh::ssh_channel_accept_forward(*raw, timeout_ms, port as *const c_int as *mut c_int)
        };
        Channel {
            raw: channel,
            session: session.clone(),
        }
    }

    pub fn open_session(&self) -> Result<(), Error> {
        let rc = unsafe { libssh::channel_open_session(self.raw) };
        self.session.eval_result(rc)
    }

    pub fn request_exec(&self, cmd: &str) -> Result<(), Error> {
        let rc = unsafe { libssh::ssh_channel_request_exec(self.raw, cmd.as_ptr() as *const i8) };
        self.session.eval_result(rc)
    }

    pub fn send_eof(&self) {
        unsafe {
            libssh::ssh_channel_send_eof(self.raw);
        }
    }
}

impl Drop for Channel {
    fn drop(&mut self) {
        unsafe {
            libssh::ssh_channel_close(self.raw);
            libssh::ssh_channel_free(self.raw);
        }
    }
}

impl std::io::Read for Channel {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        let nbytes = unsafe {
            libssh::ssh_channel_read(self.raw, buf as *mut _ as *mut c_void, buf.len() as u32, 0)
        };
        if nbytes != libssh::SSH_ERROR {
            Ok(nbytes as usize)
        } else {
            Err(std::io::Error::new(
                std::io::ErrorKind::BrokenPipe,
                "Error reading from SSH channel",
            ))
        }
    }
}

impl std::io::Write for Channel {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let nwritten = unsafe {
            libssh::ssh_channel_write(self.raw, buf as *const _ as *const c_void, buf.len() as u32)
        };
        if nwritten != libssh::SSH_ERROR {
            Ok(nwritten as usize)
        } else {
            Err(std::io::Error::new(
                std::io::ErrorKind::BrokenPipe,
                "Error writing to SSH channel",
            ))
        }
    }
    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

pub struct Bind {
    raw: server::ssh_bind,
}

impl Bind {
    pub fn new() -> Self {
        unsafe {
            Bind {
                raw: server::ssh_bind_new(),
            }
        }
    }
    pub fn set_rsa_key_path(&self, path: &str) {
        let path = CString::new(path).unwrap();
        self.set_ssh_option(
            server::ssh_bind_options_e_SSH_BIND_OPTIONS_RSAKEY,
            path.as_ptr(),
        );
    }

    pub fn set_rsa_key(&self, key: &Key) {
        self.set_ssh_option(
            server::ssh_bind_options_e_SSH_BIND_OPTIONS_IMPORT_KEY,
            key.raw as *const _,
        );
    }

    pub fn listen(&self) -> Result<(), Error> {
        let rc = unsafe { server::ssh_bind_listen(self.raw) };
        self.eval_result(rc)
    }

    pub fn accept(&self, session: &Session) -> Result<(), Error> {
        let raw_session = session.raw();
        let rc = unsafe { server::ssh_bind_accept(self.raw, *raw_session as server::ssh_session) };
        self.eval_result(rc)
    }

    pub fn set_port(&self, port: &usize) {
        self.set_ssh_option(
            server::ssh_bind_options_e_SSH_BIND_OPTIONS_BINDPORT,
            port as *const usize as *const c_void,
        );
    }

    pub fn set_verbosity(&self, level: &str) {
        let level = CString::new(level).unwrap();
        self.set_ssh_option(
            server::ssh_bind_options_e_SSH_BIND_OPTIONS_LOG_VERBOSITY_STR,
            level.as_ptr(),
        )
    }

    pub fn set_ssh_option<T>(&self, option: server::ssh_bind_options_e, value: *const T) {
        let rc = unsafe { server::ssh_bind_options_set(self.raw, option, value as *const c_void) };
        if rc != 0 {
            panic!("Error {} setting ssh bind option", rc);
        }
    }

    fn eval_result(&self, rc: i32) -> Result<(), Error> {
        if rc == libssh::SSH_OK as i32 {
            Ok(())
        } else {
            Err(Error::from_bind(&self))
        }
    }
}

pub struct SshEvent {
    raw: libssh::ssh_event,
}

impl SshEvent {
    pub fn new() -> Self {
        SshEvent {
            raw: unsafe { libssh::ssh_event_new() },
        }
    }
}

impl Drop for SshEvent {
    fn drop(&mut self) {
        unsafe { libssh::ssh_event_free(self.raw) }
    }
}

#[derive(Error, Debug)]
#[error("SSH error {code}: {msg}")]
pub struct Error {
    code: i32,
    msg: String,
}

impl Error {
    pub fn from_session(session: &Session) -> Self {
        Error::from_prt(*session.raw() as *mut c_void)
    }
    pub fn from_bind(bind: &Bind) -> Self {
        Error::from_prt(bind.raw as *mut c_void)
    }
    fn from_prt(ptr: *mut c_void) -> Self {
        unsafe {
            let code = libssh::ssh_get_error_code(ptr);
            let msg = std::ffi::CStr::from_ptr(libssh::ssh_get_error(ptr));
            let msg = msg
                .to_str()
                .unwrap_or("Could not extract message")
                .to_owned();
            Error { code, msg }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_works() {
        assert!(true);
    }
}
